HPy · Unit 1 Review

1.3 Functions

Basic Programming Constructs · calling, writing, and testing your own logic

FRQ · Warm-Up

What is a function?

In your own words: what is a function, and why would you write one? You've already been calling functions like print() and type(), think about what they have in common.

1.3.3 Writing Your Own Functions

Give a chunk of code a name

Every function you've called so far, print(), type(), len(), was written by someone else. Now you'll write your own: a named, reusable piece of code that takes some inputs and (usually) hands back a result.

  • A function is defined once, with def.
  • It can be called as many times as you like, with different inputs each time.
  • A call to a function is itself a value, whatever the function returns.
Try This

Defining and calling a function

def creates a new function named cubed. It isn't run yet, Python just remembers it. The function only runs when it is called, once per print(cubed(…)) below.

main.py
1def cubed(n):
2    return n ** 3
3print(cubed(2))
4print(cubed(3))

Line 1 only defines cubed; nothing runs until line 3 calls it with 2, and each call jumps back into line 2 to compute its own result before print() shows it.

console
8 27
Anatomy

The function header, 4 parts

main.py
def cubed(n):
def
the keyword that starts a definition
cubed
the function name
(n)
the parameters, in parentheses
:
a colon, always
The Function Body

What happens after the colon

  • Everything indented under the header is the function body.
  • A return statement immediately ends the function and hands back that value to the caller.
  • If the function finishes with no return, Python automatically returns None.
main.py
def cubed(n):
    return n ** 3  # body: 1 indented line
MCQ

Parameters vs arguments

main.py
def f(x, y):
    return x + y

print(f(2, 3))

What are the parameters, and what are the arguments?

  • A 2 and 3 are parameters, and x and y are arguments.
  • B 2 and 3 are both the parameters and arguments.
  • C x and y are parameters, and 2 and 3 are arguments.
  • D x and y are both the parameters and arguments.
More Than One Input

Parameters in order

sumOfSquares takes two parameters, x and y. When you call it, you must supply an argument for each parameter, in the same order they were defined.

main.py
def sumOfSquares(x, y):
    return x**2 + y**2

print(sumOfSquares(3, 4))
→ prints 25, since 3² + 4² is 9 + 16
Zero Inputs

Functions that take no parameters

A function doesn't have to take any parameters. But the parentheses are still required, both when you define it and when you call it.

main.py
def bigNumber():
    return 78238477823487248727834

print(bigNumber())  # still need ()
One More Trick

Returning multiple values

Separate the returned values with a comma. When you call the function, you can unpack them into that many variables at once.

main.py
1def sumAndProduct(x, y):
2    return x+y, x*y
3m, n = sumAndProduct(2, 3)
4print(m, n)
m
5
n
6
console
5 6
MCQ

How long can a function body be?

True or False: the body of a function can only contain one line.

  • A True
  • B False
Activity

Trace the Call

double(n) is defined below. Each line on the next slide calls it and stores the result in a variable. Work out what each call returns, then drag (or click, then click the box) the matching value chip into the box. Later calls build on earlier results, so trace them in order.

Activity · Trace the Call
main.py

All three calls traced correctly. A function call is a value, whatever it returns, and that value can feed straight into the next call.

Recap

Writing your own functions

  • A header is def, a name, parenthesized parameters, and a colon; the indented lines after it are the body.
  • return ends the function immediately and hands back a value; no return means the function hands back None.
  • Parameters are named in the definition; arguments are the actual values you pass in when you call it, matched up in order.
  • A function can take zero parameters (parentheses still required) or return several values at once, comma-separated.
1.3.5 Print Versus Return

A very common mistake

print() and return both seem to "give you back" a value, so it's tempting to mix them up. They do very different jobs: one displays text in the console, the other hands a value back to whoever called the function.

Try This

This looks fine…

cubed prints its answer instead of returning it. Calling it on its own line still shows 8 in the console, so at first glance it seems to work.

main.py
1def cubed(x):
2    print(x**3) # the error!
3cubed(2)
console
8
Now Watch This

Wrap the call in print()

cubed(2) still prints 8 from inside the function, but then the outer print() also prints whatever cubed returned.

main.py
1def cubed(x):
2    print(x**3) # the error!
3print(cubed(2))
console
8 None

Two separate prints: the one inside the function, and the one printing what the function returned.

Proof

The crash that proves it

Store the result and try to use it. x is None, and you cannot add 1 to None.

main.py
1def cubed(x):
2    print(x**3)
3x = cubed(2)
4print(x + 1)
console
8
x
None
console
Traceback (most recent call last):
  File "main.py", line 4
    print(x + 1)
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
The Fix

Just use return

main.py
def cubed(x):
    return x**3  # fixed

x = cubed(2)
print(x + 1)  # 9, whew!
Calling print has no effect on a return value. Printing something inside a function only sends text to the console, it never sends a value back to the caller. If you want the caller to be able to use the result, you must return it.
MCQ

No print, no return, what happens?

main.py
def f(x):
    x += 10

print(f(10))

What will this code print?

  • A 20
  • B This will crash because the function f doesn't print or return.
  • C None
  • D 10
Recap

Print versus return

  • print() displays text in the console; return hands a value back to whoever called the function.
  • A function with no return statement always hands back None, even if it prints something.
  • If you try to use a None result like a number or string, your code will crash.
1.3.4 Variable Scope

Where a variable exists

A variable exists in a specific scope, based on where it was defined. Use its name outside that scope, and Python won't recognize it. For now we'll consider two scopes: local and global.

Local Variables

Variables that live inside a function

  • A function's parameters, and any variable assigned inside its body, are local variables.
  • Local variables have local scope: they can only be used inside that one function.
main.py
def f(x):  # x is local to f
    return x + 5
Out Of Bounds

Reaching outside the scope

x only exists while f is running. Once f(4) returns, x is gone, it was never defined outside of f in the first place.

main.py
1def f(x):
2    return x + 5
3print(f(4))
4print(x)
console
9
Traceback (most recent call last):
  File "main.py", line 4
    print(x)
NameError: name 'x' is not defined
Two More Notes

Every call gets fresh locals

  • Local variables have a lifetime: they exist only during one function call. Each new call gets brand-new locals.
  • If two different functions both have a local variable x, those are different variables, even though they share a name.
square(x) has its own local x
cube(x) has a completely separate local x

Changing one never affects the other, they live in different scopes.

Global Variables

Defined at the top level

A variable assigned outside any function definition is a global variable. It has global scope: it can be used anywhere, including inside functions.

In general, do not use global variables in your own code. They can lead to a variety of obscure bugs. We use them occasionally in short notes examples for convenience, but your solutions should avoid them.
MCQ

Are parameters local variables?

True or False: parameters are local variables.

  • A True
  • B False
MCQ

Same name, same variable?

True or False: even if two functions each define a local variable x, these are still different variables.

  • A True
  • B False
Recap

Scope, at a glance

  • Local variables, parameters and anything assigned inside a function body, only exist inside that function, and only for the duration of one call.
  • Global variables, defined at the top level, can be used anywhere, but should generally be avoided.
  • Two functions can each have a local variable with the same name; they are still completely separate variables.
1.3.7 Helper Functions

Functions that help other functions

A helper function is a function like any other, except its job is to do part of the work for a different function. Breaking a problem into smaller, named pieces is one of the most effective habits you can build as a programmer.

Example

One function, calling another

largerOnesDigit doesn't compute a ones digit itself, it delegates that to onesDigit, its helper.

main.py
def onesDigit(n):
    return abs(n) % 10

def largerOnesDigit(x, y):
    return max(onesDigit(x), onesDigit(y))

print(largerOnesDigit(134, 672))  # 4
Why Bother?

Three reasons to write helpers

  • Smaller problems. It's much easier to reason about one small logical chunk at a time than one giant function.
  • Isolated testing. A helper can be tested on its own, which makes it far easier to find and fix bugs.
  • Reusability. The same helper can be called again from a completely different function later on, saving time and avoiding repeated bugs.
MCQ

How many helpers can one function have?

True or False: you can write multiple helper functions for one function.

  • A True
  • B False
Recap

Helper functions

  • A helper function does part of the work for another function, it's a regular function, used to break a big problem into small ones.
  • One function can have as many helpers as it needs, and those helpers can be reused by other functions later.
  • Embrace helper functions liberally, smaller problems are easier to solve, test, and reuse.
1.3.6 Test Functions

Debugging is the hardest part

You will spend more time debugging, finding and fixing bugs, than actually writing new code. A test function is one of the best tools you have to make that easier.

A Test Function

Testing onesDigit()

testOnesDigit's only job is to check that onesDigit works. Each assert is a test case checking one specific input.

main.py
def onesDigit(n):
    return n % 10

def testOnesDigit():
    assert(onesDigit(5) == 5)
    assert(onesDigit(123) == 3)
    assert(onesDigit(100) == 0)
    print("Passed!")

testOnesDigit()  # Passed!
How assert Works

One line, two outcomes

If the condition is True: the assert statement does nothing at all, and execution just continues to the next line.
If the condition is False: the assert statement crashes immediately with an AssertionError, telling you that test case failed.
See For Yourself

One more test case breaks it

Run this version, with assert(onesDigit(-123) == 3) added. It crashes! That's a good thing, we now know onesDigit had a bug the earlier test cases never caught.

main.py
def onesDigit(n):
    return n % 10

def testOnesDigit():
    assert(onesDigit(5) == 5)
    assert(onesDigit(123) == 3)
    assert(onesDigit(100) == 0)
    assert(onesDigit(999) == 9)
    assert(onesDigit(-123) == 3)  # we just added this case
    print('Passed!')

testOnesDigit()
Finding The Bug

% and negative numbers

Try onesDigit(-123) in the console: it returns 7, not 3. % behaves surprisingly on negative numbers: -3 % 10 is 7, not 3.

fixed· take the absolute value first
def onesDigit(n):
    return abs(n) % 10
See For Yourself

Confirm the fix

Run it and watch all five assertions pass. This doesn't guarantee onesDigit works for every possible input, but a thoughtful set of test cases gives you strong confidence that it does.

main.py
def onesDigit(n):
    return abs(n) % 10  # fixed

def testOnesDigit():
    assert(onesDigit(5) == 5)
    assert(onesDigit(123) == 3)
    assert(onesDigit(100) == 0)
    assert(onesDigit(999) == 9)
    assert(onesDigit(-123) == 3)
    print('Passed!')

testOnesDigit()
A Better assert

@testFunction

Plain assert tells you which case failed and what you expected, but not what your code actually returned. @testFunction, from cmu_cpcs_utils, fixes that, and prints "Passed!" for you automatically.

  • The @ makes this a function decorator, it modifies the behavior of the function defined right below it. More on those later.
main.py
from cmu_cpcs_utils import testFunction

@testFunction
def testOnesDigit():
    assert(onesDigit(5) == 5)
    ...
MCQ

What if we remove @testFunction?

What will happen in the previous example if we remove @testFunction?

  • A The tests will display an error, but it will not print the incorrect result the code returned.
  • B The tests will display an error, and it will print the incorrect result the code returned.
  • C The tests will not display an error, and it will print that the tests passed.
  • D The tests will not display an error. Nothing will be printed at all.
Recap

Testing your functions

  • A test function runs a set of assert statements, test cases, to check that another function returns the right result.
  • A true condition does nothing; a false one crashes with an AssertionError, that's a good thing, it means you found a bug.
  • A thoughtful set of test cases builds real confidence; a thin one can let bugs hide, as it did here.
  • @testFunction from cmu_cpcs_utils shows the incorrect result on failure, and prints "Passed!" for you.
1.3.2 Helpful Functions

Functions Python already wrote for you

Python comes with plenty of built-in functions. We won't cover anywhere near all of them, and you shouldn't go hunting for the perfect Python function to shortcut an exercise. The goal of this course is for you to become comfortable writing your own solutions, so use these sparingly.

Checking Types

type() and isinstance()

You already know type(value). isinstance(value, t) asks a yes/no question instead: does this value have type t?

main.py
1s = 'abc'
2print(type(s) == str)
3print(isinstance(s, str))
4print(isinstance(s, int))
console
True True False
Predict, Then Run

What does '12' * 5 do?

Predict the output first, then run it. s is a string, so * doesn't multiply like a number, it builds a new string out of that many copies of the original.

main.py
s = '12'
print(type(s))  # predict: what type is s?
print(s * 5)     # predict: what does this print?
Type Conversion

int() converts a value

int('12') turns the string '12' into the integer 12. Now * does ordinary multiplication.

main.py
1n = int('12')
2print(type(n))
3print(n * 5)
console
<class 'int'> 60
MCQ

int() has its limits

What is int(12.8)?

  • A 12
  • B 13
  • C 12.8
  • D This crashes.

What is int('two')?

  • A 2
  • B 'two'
  • C This crashes.
More Conversions

float(), str(), bool()

Every type has its own conversion function: float('2.5') is 2.5, and str(2.5) is '2.5'.

bool() is the most surprising one: it's False for 0, 0.0, and '' (the empty string), and True for every non-zero number and non-empty string.

bool(0)
False
bool('')
False
bool(-5)
True
Activity

Truthy or Falsy?

Drag (or click, then click the bin) each value chip into Truthy or Falsy, based on what bool() would return. A couple of these look tricky at first: check whether the value is actually 0, 0.0, or '', not just whether it looks small or empty.

Activity · Truthy or Falsy?

All 8 values sorted. bool() is False only for 0, 0.0, and the empty string, everything else is truthy.

Basic Math Functions

abs(), min(), max()

  • abs(n) returns the absolute value of n.
  • min() and max() return the smallest or largest of their arguments, and can take more than two: min(5, 1, 7) is 1.
main.py
print(abs(-5))
print(max(2, 3))
print(min(2, 3))
Trace It First

Predict each line

Click through and predict each line's output before it's revealed. Pay attention to line 2: does abs() care whether the argument is an int or a float?

main.py
1print(abs(-5))
2print(abs(-12) == abs(-12.0))
3print(min(2, 3.5))
4print(max(2, 3.5))
5print(min(5, 1, 7))
console
5 True 2 3.5 1
See For Yourself

Now check your predictions

Run the same five lines for real and compare the output against what you traced on the last slide.

main.py
print(abs(-5))
print(abs(-12) == abs(-12.0))
print(min(2, 3.5))
print(max(2, 3.5))
print(min(5, 1, 7))
Some Builtins To Avoid

pow() and round()

  • pow(2, 3) is the same as 2 ** 3. It's redundant, stick with **.
  • round() does not always round to the nearest integer. On a halfway value like 1.5 or 2.5, it rounds to the nearest even integer.
Unexpected results should be avoided. Our linter will not accept code that uses the builtin round(). We'll give you a better alternative next.
See For Yourself

Run it and look closely

Predict both lines, then run them. round(2.5) probably won't be what you expect, that's the "round-half-to-even" behavior in action.

main.py
print(round(1.5))
print(round(2.5))
A Better Toolbox

Module & CPCS Utils functions

  • math.floor(n) and math.ceil(n) return the integers just below and just above n.
  • Never compare floats with ==. Use almostEqual(x, y) from cmu_cpcs_utils instead.
  • You may also come across math.isclose(x, y). It has some gotchas, so stick with almostEqual, it will work the way you expect.
  • Need to round? Use rounded() from cmu_cpcs_utils, not the builtin round().
main.py
1print(0.1 + 0.2 == 0.3)
2print(almostEqual(0.1 + 0.2, 0.3))
3print(rounded(2.5))
console
False True 3
MCQ

Is math.isclose a safe substitute?

True or False: math.isclose and almostEqual work identically, just like pow and ** do.

  • A True
  • B False

math.isclose() breaks down for values near 0: math.isclose(0.1 + 0.2 - 0.3, 0) is False. Stick with almostEqual.

Recap

Helpful functions, used sparingly

  • isinstance(value, t) checks a type; int(), float(), str(), and bool() convert between types.
  • bool() is False only for 0, 0.0, and '', everything else is truthy.
  • Avoid the builtin pow() and round(); use ** and cmu_cpcs_utils.rounded() instead.
  • Never compare floats with ==, use almostEqual(), not math.isclose().
1.3.8 Console IO Functions

Talking to the console

"IO" stands for input and output. The console is a text area where a program's output goes, and where a user can type input back to it. We've used print() for output already; now let's round it out, and meet input().

print(), Revisited

Multiple arguments to print()

print() accepts any number of comma-separated values, and prints them on one line, each separated by a single space.

main.py
1x, y = 3, 2
2print(x, 'plus', y, 'equals', x+y)
console
3 plus 2 equals 5
MCQ

Three arguments, one line

main.py
x = 'a'
y = 'b'
z = 'c'
print(x, y, z)

What does this print?

  • A 'a b c'
  • B a, b, c
  • C abc
  • D a b c
Getting Input

input() always returns a string

input(prompt) shows prompt, waits for the user to type something and press enter, then returns exactly what they typed. Whatever they typed, even digits, comes back as a str, never a number.

main.py
name = input('Enter your name: ')
print('Your name is', name)
See For Yourself

Try (and fail) to input a number

Run this and enter 5 for the dog's age. Instead of 35, you'll see 5555555: since dogYears is a string, 7 * dogYears repeats the string, it doesn't multiply a number.

main.py
dogYears = input("Enter your dog's age in years: ")
humanYears = 7 * dogYears
print("Your dog's age in human years is", humanYears)
The Fix

Wrap it in int()

Convert the result of input() before doing math with it. Now 7 * dogYears is ordinary multiplication.

main.py
dogYears = int(input("Enter your dog's age in years: "))
humanYears = 7 * dogYears
print("Your dog's age in human years is", humanYears)

We used double quotes here because the prompt text itself contains an apostrophe, in dog's.

MCQ

What if the input isn't a number?

What will happen if you don't input a number in the fixed example?

  • A It will not crash, and it will print "Your dog's age in human years is".
  • B It will crash on line 1.
  • C It will crash on line 2.
  • D It will not crash, and nothing will print.
One Last Tip

Debugging with print()

As functions get more complex, add print statements inside them to check local variables while a failing test case runs. One line will surprise you, and that's usually where the bug is.

Hard to read: print(x) just shows a bare value, with no label.
Better: print('x:', x) labels the value, so you know which variable produced it.

Remove your debugging prints before you submit code to the autograder.

Recap

Console IO

  • print() can take multiple comma-separated values, and joins them with a single space.
  • input(prompt) shows the prompt and always returns the user's text as a str, even if it looks like a number.
  • Convert with int() or float() before doing math with input, or you'll repeat strings instead of computing.
  • Labeled print statements, print('x:', x), are one of the most effective debugging tools you have.
Unit Recap

Functions: the big picture

  • Write your own: def, parameters, a body, and return, the value a call evaluates to.
  • Print is not return. Only return hands a value back to the caller.
  • Scope keeps a function's locals private to that one call; avoid globals.
  • Helper functions break big problems into small, testable, reusable pieces.
  • Test functions, built from assert, are how you catch the bugs a thin set of examples would miss.
  • Python's own helpful functions and console IO round out your toolkit, use them, but keep writing your own logic.